Skip to content

unifed api: stt sarvam#579

Open
rajagopalmotivate wants to merge 25 commits intomainfrom
feature/unifed-api-stt-sarvam
Open

unifed api: stt sarvam#579
rajagopalmotivate wants to merge 25 commits intomainfrom
feature/unifed-api-stt-sarvam

Conversation

@rajagopalmotivate
Copy link
Collaborator

@rajagopalmotivate rajagopalmotivate commented Feb 4, 2026

Summary

Target issue is #564
Explain the motivation for making this change. What existing problem does the pull request solve?
Sarvam STT

Checklist

Before submitting a pull request, please ensure that you mark these task.

  • Ran fastapi run --reload app/main.py or docker compose up in the repository root and test.
  • If you've fixed a bug or added code that is tested and has test cases.

Notes

Please add here if any other information is required for the reviewer.

Summary by CodeRabbit

  • New Features
    • Google and SarvamAI provider support with integrated STT/TTS workflows and multimodal audio handling.
    • Multimodal input resolution (text, base64 audio, audio URL) with temp-file handling and cleanup.
    • Enhanced LLM call tracking: create, update, retrieve, and richer usage reporting (including reasoning tokens).
    • Partial configuration updates for creating new config versions (merge-and-validate behavior).

@rajagopalmotivate rajagopalmotivate self-assigned this Feb 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Walkthrough

Adds LLM call persistence and tracking, multimodal input resolution, Google and SarvamAI providers, provider registry and client factories, partial config-version creation with immutable-type checks, integrates LLM call lifecycle into job execution, and includes migrations, models, CRUD, services, and extensive tests.

Changes

Cohort / File(s) Summary
Migrations
backend/app/alembic/versions/042_add_llm_call_table.py, backend/app/alembic/versions/043_remove_enum_checks_llm_call_provider.py
Add llm_call table with FKs, conditional indexes, and column comment updates; downgrades revert changes.
Models (LLM & Config)
backend/app/models/llm/request.py, backend/app/models/llm/response.py, backend/app/models/config/version.py, backend/app/models/llm/__init__.py, backend/app/models/config/__init__.py, backend/app/models/__init__.py
Introduce multimodal inputs, new completion/config shapes (Native/Kaapi with type), ConfigVersionCreatePartial, LlmCall SQLModel, and add reasoning_tokens to usage; update exports.
CRUD & DB Logic
backend/app/crud/llm.py, backend/app/crud/config/version.py
New LLM CRUD functions (create/update/get) and partial-version creation with deep-merge, immutable-type validation, and related helpers.
Provider Core & Registry
backend/app/core/providers.py, backend/app/services/llm/providers/base.py, backend/app/services/llm/providers/registry.py, backend/app/services/llm/providers/__init__.py
Add providers to enum, require create_client on BaseProvider, refactor registry to get_provider_class/supported_providers, update mapping and credential flow.
Provider Implementations
backend/app/services/llm/providers/oai.py, backend/app/services/llm/providers/gai.py, backend/app/services/llm/providers/sai.py
OpenAI provider: add client factory and accept resolved input; add GoogleAIProvider and SarvamAIProvider implementing STT flows and create_client factories.
LLM Service & Input Resolution
backend/app/services/llm/jobs.py, backend/app/services/llm/input_resolver.py, backend/app/services/llm/mappers.py, backend/app/services/llm/__init__.py
Integrate llm_call creation/update into job execution, add input resolution for text/base64/url, add mappers including Google mapping, and adjust job/provider flows.
API & CLI
backend/app/api/routes/config/version.py, backend/app/cli/bench/commands.py
Switch version creation to partial model and create_from_partial_or_raise; minor CLI message formatting tweak.
Tests
backend/app/tests/... (many files)
Extensive test updates: new LLM CRUD tests, Google provider STT tests, partial-version/type-immutability tests, many fixtures adjusted to include type and dict-based params; several with-statement formatting changes; some tests commented out.
Formatting & Misc
backend/app/celery/*.py, backend/app/services/doctransform/zerox_transformer.py, various tests
Whitespace and minor formatting/spelling tweaks.
Dependencies
backend/pyproject.toml
Add google-genai>=1.59.0 dependency for Google provider.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as "API Route"
    participant ConfigCRUD as "Config CRUD"
    participant JobSvc as "Job Service"
    participant LLMCrud as "LLM CRUD"
    participant InputRes as "Input Resolver"
    participant Registry as "Provider Registry"
    participant Provider as "Provider"
    participant DB as "Database"

    Client->>API: POST create_version (partial)
    API->>ConfigCRUD: create_from_partial_or_raise()
    ConfigCRUD->>DB: select latest version / insert merged version
    API-->>Client: version created

    Client->>API: POST execute_job
    API->>JobSvc: start_job()
    JobSvc->>DB: insert Job
    JobSvc->>ConfigCRUD: resolve_config_blob()
    ConfigCRUD-->>JobSvc: ConfigBlob

    JobSvc->>LLMCrud: create_llm_call(...)
    LLMCrud->>DB: insert llm_call
    LLMCrud-->>JobSvc: llm_call_id

    JobSvc->>InputRes: resolve_input(QueryInput)
    InputRes-->>JobSvc: resolved_input (text | temp_file)

    JobSvc->>Registry: get_provider_class(provider_type)
    Registry-->>JobSvc: ProviderClass
    JobSvc->>Provider: execute(config, query, resolved_input)
    Provider-->>JobSvc: LLMCallResponse

    JobSvc->>LLMCrud: update_llm_call_response(llm_call_id,...)
    LLMCrud->>DB: update llm_call
    LLMCrud-->>JobSvc: updated record

    JobSvc->>InputRes: cleanup_temp_file()
    JobSvc->>DB: update Job status
    JobSvc-->>API: job complete
    API-->>Client: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • avirajsingh7
  • nishika26
  • Prajna1999

Poem

🐰 I hopped through migrations, models, and queues,

Saved calls and tokens from varied inputs and views,
Google and Sarvam lent voices to play,
Jobs spawn records that brighten the way,
Temp files vanish — the rabbit hops away.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'unifed api: stt sarvam' is vague and contains a typo ('unifed' instead of 'unified'). While it references Sarvam STT, it lacks clarity about the actual scope of changes, which extends well beyond just Sarvam—including Google AI provider, LLM call tracking, config versioning, and comprehensive infrastructure changes. Revise the title to be more specific and accurate, such as 'Add multimodal LLM support with STT providers (Sarvam, Google) and LLM call tracking' or 'Implement unified LLM API with STT/TTS support and call history'. Fix the typo 'unifed' → 'unified'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 84.08% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/unifed-api-stt-sarvam

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
backend/app/services/doctransform/zerox_transformer.py (1)

20-47: ⚠️ Potential issue | 🟡 Minor

Prefix all log messages with the function name.

Current log entries in transform() don’t include the required [ZeroxTransformer.transform] prefix. Please align them with the logging guideline.
As per coding guidelines, "Prefix all log messages with the function name in square brackets: logger.info(f"[function_name] Message {mask_string(sensitive_value)}")".

Suggested fix
-        logger.info(f"ZeroxTransformer Started: (model={self.model})")
+        logger.info(f"[ZeroxTransformer.transform] ZeroxTransformer Started: (model={self.model})")
...
-            logger.error(
-                f"ZeroxTransformer timed out for {input_path} (model={self.model})"
-            )
+            logger.error(
+                f"[ZeroxTransformer.transform] ZeroxTransformer timed out for {input_path} (model={self.model})"
+            )
...
-            logger.error(
-                f"ZeroxTransformer failed for {input_path}: {e}\n"
-                "This may be due to a missing Poppler installation or a corrupt PDF file.",
-                exc_info=True,
-            )
+            logger.error(
+                f"[ZeroxTransformer.transform] ZeroxTransformer failed for {input_path}: {e}\n"
+                "This may be due to a missing Poppler installation or a corrupt PDF file.",
+                exc_info=True,
+            )
backend/app/tests/services/doctransformer/test_job/utils.py (2)

73-99: ⚠️ Potential issue | 🟡 Minor

Add type hints to factory helpers and nested callables.

The factory functions create_failing_convert_document and create_persistent_failing_convert_document, along with their nested callables, are missing type annotations. This violates the coding guideline requiring type hints on all function parameters and return values.

Suggested fix

Add imports:

+from collections.abc import Callable
+from typing import NoReturn

Apply type hints:

-def create_failing_convert_document(fail_count: int = 1):
+def create_failing_convert_document(fail_count: int = 1) -> Callable[..., Path]:
     """Create a side effect function that fails specified times then succeeds."""
     call_count = 0
 
-    def failing_convert_document(*args, **kwargs):
+    def failing_convert_document(*args: object, **kwargs: object) -> Path:
         nonlocal call_count
         call_count += 1
         if call_count <= fail_count:
             raise Exception("Transient error")
         output_path = args[1] if len(args) > 1 else kwargs.get("output_path")
         if output_path:
             output_path.write_text("Success after retries", encoding="utf-8")
             return output_path
         raise ValueError("output_path is required")
 
     return failing_convert_document


-def create_persistent_failing_convert_document(
-    error_message: str = "Persistent error",
-):
+def create_persistent_failing_convert_document(
+    error_message: str = "Persistent error",
+) -> Callable[..., NoReturn]:
     """Create a side effect function that always fails."""
 
-    def persistent_failing_convert_document(*args, **kwargs):
+    def persistent_failing_convert_document(*args: object, **kwargs: object) -> NoReturn:
         raise Exception(error_message)
 
     return persistent_failing_convert_document

53-58: ⚠️ Potential issue | 🟡 Minor

Add optional type hint to expected_content parameter.

The parameter defaults to None, so the type should allow None to satisfy type checking. Update the type hint to str | None.

Proposed fix
-        expected_content: str = None,
+        expected_content: str | None = None,

This aligns with the coding guideline to always add type hints to all function parameters and return values in Python code.

backend/app/tests/crud/config/test_config.py (1)

19-31: ⚠️ Potential issue | 🟡 Minor

Add a return type hint (and consider a factory-style fixture).
The fixture is missing a return type annotation, and the test guideline prefers factory-pattern fixtures.

🔧 Proposed fix for the return type hint
-@pytest.fixture
-def example_config_blob():
+@pytest.fixture
+def example_config_blob() -> ConfigBlob:
     return ConfigBlob(
         completion=NativeCompletionConfig(
             provider="openai-native",
             type="text",
             params={
                 "model": "gpt-4",
                 "temperature": 0.8,
                 "max_tokens": 1500,
             },
         )
     )
As per coding guidelines “Always add type hints to all function parameters and return values in Python code” and “backend/app/tests/**/*.py: Use factory pattern for test fixtures in backend/app/tests/”.
🤖 Fix all issues with AI agents
In `@backend/app/alembic/versions/042_add_llm_call_table.py`:
- Around line 1-16: Update the module header docstring so the human-readable
Revision ID and Revises lines match the actual alembic variables: change the
header "Revision ID: 041" to "Revision ID: 042" and "Revises: 040" to "Revises:
041" to align with the `revision = "042"` and `down_revision = "041"` values in
this migration file (look for the top-of-file docstring and the
`revision`/`down_revision` symbols).
- Around line 20-21: Add explicit return type annotations to the Alembic
migration functions: annotate both upgrade() and downgrade() with -> None (e.g.,
def upgrade() -> None: and def downgrade() -> None:) so they satisfy the
project's typing requirements; update the function signatures where upgrade and
downgrade are defined in this migration file.

In `@backend/app/alembic/versions/043_remove_enum_checks_llm_call_provider.py`:
- Around line 20-30: The column comment on the "provider" column in the
upgrade() migration contains a likely typo ("AI provider as sent by user (e.g
openai, -native, google)"); update the comment to the correct provider token
(e.g. "AI provider as sent by user (e.g. openai, openai-native, google)" or
whichever canonical name is used) inside the op.alter_column call in upgrade(),
ensuring the existing_comment/existing_type/nullable parameters remain unchanged
and only the comment string is corrected.

In `@backend/app/api/routes/config/version.py`:
- Around line 31-41: Update the ConfigVersionCreatePartial model docstring in
backend/app/models/config/version.py to correctly state immutability: change the
incorrect sentence that says "provider and type are inherited... and cannot be
changed" to reflect that only "type is inherited from the existing config and
cannot be changed," while "provider, model, and params can be changed"; ensure
the docstring on the ConfigVersionCreatePartial class matches the route
docstring and the behavior enforced by _validate_immutable_fields.

In `@backend/app/crud/llm.py`:
- Around line 143-146: The log statement in create_llm_call uses an undefined
variable provider which will raise a NameError; update the logger.info call that
logs db_llm_call.id, job_id, provider, model to use the function parameter
original_provider instead of provider (i.e., reference original_provider in the
message) so the runtime uses the correct variable and the log prints the
provider value.

In `@backend/app/models/llm/request.py`:
- Around line 187-198: The model validator method validate_params in class
KaapiCompletionConfig (annotated with `@model_validator`(mode="after")) lacks a
return type; update its signature to include -> "KaapiCompletionConfig" so it
reads def validate_params(self) -> "KaapiCompletionConfig": (keep the body
unchanged and continue returning self) to satisfy the project's type-hinting
guideline.
- Around line 4-11: Remove the duplicate imports of Field and SQLModel (there
are two sqlmodel import lines in the top imports) so each symbol is imported
only once, and add explicit return type annotations "-> Self" to the three
validator methods validate_conversation_logic, validate_params, and
validate_config_logic (and import Self from typing or typing_extensions if not
already available) because they return self; ensure the validator signatures are
updated accordingly.

In `@backend/app/services/llm/jobs.py`:
- Around line 205-206: Remove the unused variable assignment
user_sent_config_provider = "" to fix the Ruff F841 lint error: locate the
assignment of user_sent_config_provider and delete that line (or replace it with
intended usage if it was meant to be used later in the function / job handler).
- Around line 287-312: The cleanup block currently unconditionally calls
cleanup_temp_file when resolved_input differs from request.query.input, which
can treat plain text as a file path; change it to only call cleanup_temp_file
when resolved_input is an audio-derived object created by the resolver (e.g.,
check isinstance(resolved_input, (AudioBase64Input, AudioUrlInput)) or
equivalent sentinel the resolver returns) and add imports for AudioBase64Input
and AudioUrlInput from app.models.llm.request so the isinstance check resolves;
keep the existing compare to request.query.input as an extra guard if desired.

In `@backend/app/services/llm/mappers.py`:
- Around line 67-73: The code sets
openai_params["tools"][...]["max_num_results"] using "max_num_results or 20",
which overwrites an explicit 0; change the assignment to preserve falsy but
valid values by using an explicit None check (e.g. use max_num_results if
max_num_results is not None else 20) when populating openai_params["tools"] so
that a passed 0 remains 0; update the block that builds the "tools" dict (the
code that references openai_params["tools"], "type": "file_search", and the
max_num_results key) accordingly.

In `@backend/app/services/llm/providers/gai.py`:
- Around line 22-44: Add missing type annotations and an explicit error path:
annotate GoogleAIProvider.__init__ with a return type of -> None, and update
_parse_input signature to include types (e.g., query_input: Any,
completion_type: str, provider: str) and keep -> str; change the function so
that if completion_type != "stt" it raises a ValueError (instead of implicitly
returning None), and keep the existing branch that returns the string when
query_input is str and raises when it's not (preserve the ValueError message
f"{provider} STT require file path").
- Around line 136-152: The execute method in gai.GAIProvider currently only
returns for completion_type == "stt" and falls through for "text" or "tts",
causing implicit None returns; update execute (method name execute, variable
completion_type) to explicitly return a (None, error_message) tuple for
unsupported/non-handled completion types (e.g., "text" and "tts") after the
existing if-block, using a clear error string (include completion_type) so
callers that do result, error = instance.execute(...) always get a tuple; keep
existing _execute_stt call unchanged.

In `@backend/app/services/llm/providers/registry.py`:
- Around line 97-140: Remove the ad-hoc test harness in the module (the entire
if __name__ == "__main__": block that constructs mock_credentials, calls
LLMProvider.get_provider_class, ProviderClass.create_client, instantiates
ProviderClass, builds NativeCompletionConfig and QueryParams, and calls
instance.execute); either delete it from registry.py or move it into a separate
test/debug script under tests/ or scripts/ and replace print calls with proper
logging in that script; ensure production code (registry.py and functions like
LLMProvider.get_provider_class, ProviderClass.create_client, and
instance.execute) contains no hardcoded paths, getenv usage, or console prints
left behind.
- Around line 67-72: The variable credential_provider is assigned from
provider_type.replace("-native", "") twice (duplicate lines); remove the
redundant assignment so credential_provider is set only once (keep the single
assignment and its explanatory comment) and ensure no other logic depends on the
duplicate; update the block where credential_provider and provider_type are
handled to contain a single assignment to credential_provider.

In `@backend/app/services/llm/providers/sai.py`:
- Around line 95-115: Guard against sarvam_response.transcript being None before
calling .split(): when constructing output_tokens_estimate and
LLMOutput(text=...), use a safe transcript variable (e.g., transcript =
sarvam_response.transcript or "") or conditional logic so you never call
.split() on None; then compute output_tokens_estimate = len(transcript.split())
and pass transcript into LLMOutput and provider_response_id fallback logic in
LLMResponse and Usage creation (LLMCallResponse, LLMResponse, Usage).
- Around line 22-25: Remove the hardcoded SARVAM_API_KEY and the print call:
stop setting SARVAM_API_KEY to a fallback secret and remove the stdout print;
instead, rely on os.getenv to provide the key and, if you want a warning in
SarvamAIProvider.__init__, call logger.warning(f"[SarvamAIProvider.__init__]
SARVAM_API_KEY not set") (use mask_string when including any partial secret)
rather than printing or embedding a real key in source; ensure no default secret
value remains anywhere in the module.
- Around line 30-38: The __init__ method in class SarvamAIProvider lacks an
explicit return type; update the SarvamAIProvider.__init__ signature to include
the return annotation "-> None" (i.e., def __init__(self, client: SarvamAI) ->
None:) to comply with the project's function return type guideline, keeping the
rest of the body (super().__init__(client); self.client = client) unchanged.

In `@backend/app/tests/services/llm/test_mappers.py`:
- Around line 1-317: The test module has been entirely commented out which
removes coverage; restore the tests by uncommenting the file and its imports and
test classes (references: KaapiLLMParams, KaapiCompletionConfig,
NativeCompletionConfig, map_kaapi_to_openai_params,
transform_kaapi_config_to_native) so they run normally, or if intentionally
disabled mark individual tests with pytest.mark.skip and add a TODO/link to a
tracked issue; ensure pytest is imported and any mocked tests (e.g., using
MagicMock) remain valid after re-enabling so the CI regains these checks.

In `@backend/app/tests/utils/test_data.py`:
- Around line 342-343: The code reads completion_config.get("type") into
config_type without a default which can lead to Pydantic validation errors for
NativeCompletionConfig/KaapiCompletionConfig; set a safe default (e.g., "text")
when reading the field so config_type = completion_config.get("type", "text")
(or equivalent) before passing into NativeCompletionConfig/KaapiCompletionConfig
construction to ensure the required "type" is always present and one of
"text"/"stt"/"tts".
🧹 Nitpick comments (15)
backend/app/tests/utils/test_data.py (2)

360-373: Provider value may not match Kaapi Literal types.

The else branch assumes any provider not in ["openai-native", "google-native"] is a valid Kaapi provider, but KaapiCompletionConfig.provider is Literal["openai", "google"]. If provider has an unexpected value (e.g., "sarvam" or a typo), this will fail Pydantic validation.

Consider adding explicit validation or a mapping for edge cases.

💡 Optional: Add explicit provider handling
             else:
                 # For Kaapi providers (openai, google)
+                kaapi_provider = provider if provider in ["openai", "google"] else "openai"
                 config_blob = ConfigBlob(
                     completion=KaapiCompletionConfig(
-                        provider=provider,
+                        provider=kaapi_provider,
                         type=config_type,
                         params={

321-337: Imports inside function body.

Moving select, and_, and ConfigVersion imports inside the function works but deviates from typical module-level imports. This is acceptable in test utilities to avoid circular imports, but ensure this pattern is intentional.

backend/app/celery/worker.py (2)

14-18: Consider using Optional[int] for the type hint.

The parameter concurrency: int = None is technically incorrect since None is not an int. For clarity and type checker compliance, consider using Optional[int].

Suggested fix
+from typing import Optional
+
 def start_worker(
     queues: str = "default,high_priority,low_priority,cron",
-    concurrency: int = None,
+    concurrency: Optional[int] = None,
     loglevel: str = "info",
 ):

30-31: Log messages should follow the [function_name] prefix pattern.

As per coding guidelines, log messages should be prefixed with the function name in square brackets.

Suggested fix
-    logger.info(f"Starting Celery worker with {concurrency} processes")
-    logger.info(f"Consuming queues: {queues}")
+    logger.info(f"[start_worker] Starting Celery worker with {concurrency} processes")
+    logger.info(f"[start_worker] Consuming queues: {queues}")
backend/app/celery/beat.py (1)

6-6: Unused import: Celery is imported but never used.

The Celery class is imported but only celery_app (from line 7) is used in this module.

Suggested fix
 import logging
-from celery import Celery
 from app.celery.celery_app import celery_app
backend/app/alembic/versions/043_remove_enum_checks_llm_call_provider.py (1)

20-21: Add return type hints to upgrade() and downgrade() functions.

Per coding guidelines, all functions should have type hints. Alembic migration functions should return None.

Suggested fix
-def upgrade():
+def upgrade() -> None:
     # ### commands auto generated by Alembic - please adjust! ###
-def downgrade():
+def downgrade() -> None:
     # ### commands auto generated by Alembic - please adjust! ###

Also applies to: 33-34

backend/app/tests/seed_data/seed_data.py (1)

21-22: Unused imports: Config and ConfigVersion are imported but never used.

These models are imported but not referenced anywhere in the file. If they were added for future use or to ensure model registration, consider adding a comment explaining the intent. Otherwise, they should be removed to keep imports clean.

🧹 Proposed fix to remove unused imports
 from app.models import (
     APIKey,
     Organization,
     Project,
     User,
     Credential,
     Assistant,
     Document,
-    Config,
-    ConfigVersion,
 )
backend/app/models/config/version.py (1)

99-116: Consider adding validation for empty config_blob.

Unlike ConfigVersionBase which has validate_blob_not_empty, this model allows an empty config_blob. While this might be intentional (to allow creating a new version with only a commit_message change), it could lead to confusion. If empty blobs should be rejected, add similar validation:

from pydantic import field_validator

`@field_validator`("config_blob")
def validate_blob_not_empty(cls, value):
    if not value:
        raise ValueError("config_blob cannot be empty")
    return value

If allowing empty blobs is intentional (inheriting everything from the previous version), consider documenting this behavior explicitly in the docstring.

backend/app/tests/crud/test_llm.py (1)

42-46: Add return type hint to fixture.

The test_job fixture is missing a return type annotation. Per coding guidelines, all function parameters and return values should have type hints.

Proposed fix
 `@pytest.fixture`
-def test_job(db: Session):
+def test_job(db: Session) -> "Job":
     """Create a test job for LLM call tests."""
     crud = JobCrud(db)
     return crud.create(job_type=JobType.LLM_API, trace_id="test-llm-trace")

You'll need to import Job from app.models or use a string annotation if there are circular import concerns.

backend/app/tests/api/routes/configs/test_version.py (1)

471-519: Remove unused import.

TextLLMParams is imported on line 477 but never used in this test function.

Proposed fix
 def test_create_version_cannot_change_type_from_text_to_stt(
     db: Session,
     client: TestClient,
     user_api_key: TestAuthContext,
 ) -> None:
     """Test that config type cannot be changed from 'text' to 'stt' in a new version."""
-    from app.models.llm.request import KaapiCompletionConfig, TextLLMParams
+    from app.models.llm.request import KaapiCompletionConfig
backend/app/services/llm/providers/registry.py (2)

93-94: Log message should include function name prefix.

Per coding guidelines, log messages should be prefixed with the function name in square brackets.

Proposed fix
-        logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
+        logger.error(f"[get_llm_provider] Failed to initialize {provider_type} client: {e}", exc_info=True)

13-26: Review temporary imports before merging.

The comment on line 15 indicates these are "temporary imports". Several imports appear unused in the production code path (GenerateContentConfig, LLMCallResponse, LLMOutput, LLMResponse, Usage) and are only used by the ad hoc testing block.

Once the testing code is removed, these imports should also be cleaned up along with load_dotenv() and os.

Expected cleanup after removing test code
-import os
-from dotenv import load_dotenv
 import logging

 from sqlmodel import Session
-from openai import OpenAI

 from app.crud import get_provider_credential
 from app.services.llm.providers.base import BaseProvider
 from app.services.llm.providers.oai import OpenAIProvider
 from app.services.llm.providers.gai import GoogleAIProvider

-from google.genai.types import GenerateContentConfig
-
-# temporary import
-
-from app.models.llm import (
-    NativeCompletionConfig,
-    LLMCallResponse,
-    QueryParams,
-    LLMOutput,
-    LLMResponse,
-    Usage,
-)
-
-load_dotenv()

 logger = logging.getLogger(__name__)
backend/app/services/llm/input_resolver.py (1)

86-111: Consider streaming for large audio files and safer error handling.

Two potential improvements:

  1. Memory usage: response.content loads the entire file into memory. For large audio files, this could cause memory issues. Consider using stream=True with chunked writing.

  2. HTTPError safety: On line 94, e.response could theoretically be None in edge cases, though this is rare with raise_for_status().

Proposed fix with streaming support
 def resolve_audio_url(url: str) -> tuple[str, str | None]:
     """Fetch audio from URL and write to temp file. Returns (file_path, error)."""
     try:
-        response = requests.get(url, timeout=60)
+        response = requests.get(url, timeout=60, stream=True)
         response.raise_for_status()
     except requests.Timeout:
         return "", f"Timeout fetching audio from URL: {url}"
     except requests.HTTPError as e:
-        return "", f"HTTP error fetching audio: {e.response.status_code}"
+        status = e.response.status_code if e.response else "unknown"
+        return "", f"HTTP error fetching audio: {status}"
     except Exception as e:
         return "", f"Failed to fetch audio from URL: {str(e)}"

     content_type = response.headers.get("content-type", "audio/wav")
     ext = get_file_extension(content_type.split(";")[0].strip())

     try:
         with tempfile.NamedTemporaryFile(
             suffix=ext, delete=False, prefix="audio_"
         ) as tmp:
-            tmp.write(response.content)
+            for chunk in response.iter_content(chunk_size=8192):
+                if chunk:
+                    tmp.write(chunk)
             temp_path = tmp.name

         logger.info(f"[resolve_audio_url] Wrote audio to temp file: {temp_path}")
         return temp_path, None
     except Exception as e:
         return "", f"Failed to write fetched audio to temp file: {str(e)}"
backend/app/services/llm/jobs.py (1)

161-178: Consider gating the debug job dump.

Fetching and logging recent jobs on a miss can be noisy in production; consider a debug flag or removal once the issue is resolved.

backend/app/tests/services/llm/providers/test_gai.py (1)

52-81: Add fixture type hints and consider factory-style fixtures.

Type hints keep tests consistent with typing guidance, and factory fixtures improve reuse when variations are needed.

✍️ Example adjustment
-    def mock_client(self):
+    def mock_client(self) -> MagicMock:
@@
-    def provider(self, mock_client):
+    def provider(self, mock_client: MagicMock) -> GoogleAIProvider:
@@
-    def stt_config(self):
+    def stt_config(self) -> NativeCompletionConfig:
@@
-    def query_params(self):
+    def query_params(self) -> QueryParams:
As per coding guidelines: **/*.py: Always add type hints to all function parameters and return values in Python code; backend/app/tests/**/*.py: Use factory pattern for test fixtures in backend/app/tests/.

Comment on lines +1 to +16
"""add_llm_call_table

Revision ID: 041
Revises: 040
Create Date: 2026-01-26 15:20:23.873332

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "042"
down_revision = "041"
branch_labels = None
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the revision header to match the revision variables.
The header says “Revision ID: 041” and “Revises: 040” while the actual revision = "042" and down_revision = "041". This mismatch is confusing and suggests the migration wasn’t generated with the expected rev‑id.

🔧 Suggested header fix
-Revision ID: 041
-Revises: 040
+Revision ID: 042
+Revises: 041

Based on learnings: Generate database migrations using alembic revision --autogenerate -m "Description" --rev-id <number> where rev-id is the latest existing revision ID + 1.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""add_llm_call_table
Revision ID: 041
Revises: 040
Create Date: 2026-01-26 15:20:23.873332
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "042"
down_revision = "041"
branch_labels = None
"""add_llm_call_table
Revision ID: 042
Revises: 041
Create Date: 2026-01-26 15:20:23.873332
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "042"
down_revision = "041"
branch_labels = None
🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/042_add_llm_call_table.py` around lines 1 - 16,
Update the module header docstring so the human-readable Revision ID and Revises
lines match the actual alembic variables: change the header "Revision ID: 041"
to "Revision ID: 042" and "Revises: 040" to "Revises: 041" to align with the
`revision = "042"` and `down_revision = "041"` values in this migration file
(look for the top-of-file docstring and the `revision`/`down_revision` symbols).

Comment on lines +20 to +21
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add return type hints for upgrade and downgrade.
These functions should be explicitly annotated to satisfy the project’s typing requirement.

✅ Suggested fix
-def upgrade():
+def upgrade() -> None:
@@
-def downgrade():
+def downgrade() -> None:

As per coding guidelines: Always add type hints to all function parameters and return values in Python code.

Also applies to: 164-165

🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/042_add_llm_call_table.py` around lines 20 - 21,
Add explicit return type annotations to the Alembic migration functions:
annotate both upgrade() and downgrade() with -> None (e.g., def upgrade() ->
None: and def downgrade() -> None:) so they satisfy the project's typing
requirements; update the function signatures where upgrade and downgrade are
defined in this migration file.

Comment on lines +20 to +30
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"llm_call",
"provider",
existing_type=sa.VARCHAR(),
comment="AI provider as sent by user (e.g openai, -native, google)",
existing_comment="AI provider: openai, google, anthropic",
existing_nullable=False,
)
# ### end Alembic commands ###
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential typo in the column comment: -native may be incomplete.

The comment "AI provider as sent by user (e.g openai, -native, google)" contains -native which appears incomplete. Should this be openai-native or google-native based on the provider naming convention?

Suggested fix (if `openai-native` was intended)
     op.alter_column(
         "llm_call",
         "provider",
         existing_type=sa.VARCHAR(),
-        comment="AI provider as sent by user (e.g openai, -native, google)",
+        comment="AI provider as sent by user (e.g. openai, openai-native, google)",
         existing_comment="AI provider: openai, google, anthropic",
         existing_nullable=False,
     )
🤖 Prompt for AI Agents
In `@backend/app/alembic/versions/043_remove_enum_checks_llm_call_provider.py`
around lines 20 - 30, The column comment on the "provider" column in the
upgrade() migration contains a likely typo ("AI provider as sent by user (e.g
openai, -native, google)"); update the comment to the correct provider token
(e.g. "AI provider as sent by user (e.g. openai, openai-native, google)" or
whichever canonical name is used) inside the op.alter_column call in upgrade(),
ensuring the existing_comment/existing_type/nullable parameters remain unchanged
and only the comment string is corrected.

Comment on lines 31 to +41
"""
Create a new version for an existing configuration.
The version number is automatically incremented.

Only include the fields you want to update in config_blob.
Provider, model, and params can be changed.
Type is inherited from existing config and cannot be changed.
"""
version_crud = ConfigVersionCrud(
session=session, project_id=current_user.project_.id, config_id=config_id
)
version = version_crud.create_or_raise(version_create=version_create)
version = version_crud.create_from_partial_or_raise(version_create=version_create)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find _validate_immutable_fields implementation
rg -n -A 25 "_validate_immutable_fields" --type=py backend/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 4079


🏁 Script executed:

# Find ConfigVersionCreatePartial model definition
rg -n -B 5 -A 15 "class ConfigVersionCreatePartial" --type=py backend/

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1662


🏁 Script executed:

# Check the version.py file to understand the context better
cat -n backend/app/api/routes/config/version.py | head -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1966


Correct the ConfigVersionCreatePartial model docstring to accurately reflect immutable fields.

The route docstring correctly states "Provider, model, and params can be changed. Type is inherited from existing config and cannot be changed," which matches the actual behavior in _validate_immutable_fields (which only validates type immutability). However, the ConfigVersionCreatePartial model docstring in backend/app/models/config/version.py incorrectly claims that both "provider and type are inherited... and cannot be changed" when in fact only type is immutable. Update the model docstring to reflect that provider and type have different mutability constraints.

🤖 Prompt for AI Agents
In `@backend/app/api/routes/config/version.py` around lines 31 - 41, Update the
ConfigVersionCreatePartial model docstring in
backend/app/models/config/version.py to correctly state immutability: change the
incorrect sentence that says "provider and type are inherited... and cannot be
changed" to reflect that only "type is inherited from the existing config and
cannot be changed," while "provider, model, and params can be changed"; ensure
the docstring on the ConfigVersionCreatePartial class matches the route
docstring and the behavior enforced by _validate_immutable_fields.

Comment on lines +143 to +146
logger.info(
f"[create_llm_call] Created LLM call id={db_llm_call.id}, "
f"job_id={job_id}, provider={provider}, model={model}"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n backend/app/crud/llm.py | head -160 | tail -30

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1020


🏁 Script executed:

cat -n backend/app/crud/llm.py | head -130 | tail -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1983


🏁 Script executed:

cat -n backend/app/crud/llm.py | head -80 | tail -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1859


Fix undefined provider in log message.

The log message references an undefined provider variable. The function parameter is original_provider (line 65), which should be used instead. This will cause a NameError at runtime when the log is executed, even though the database write succeeds.

Suggested fix
-        f"[create_llm_call] Created LLM call id={db_llm_call.id}, "
-        f"job_id={job_id}, provider={provider}, model={model}"
+        f"[create_llm_call] Created LLM call id={db_llm_call.id}, "
+        f"job_id={job_id}, provider={original_provider}, model={model}"
🧰 Tools
🪛 Ruff (0.14.14)

[error] 145-145: Undefined name provider

(F821)

🤖 Prompt for AI Agents
In `@backend/app/crud/llm.py` around lines 143 - 146, The log statement in
create_llm_call uses an undefined variable provider which will raise a
NameError; update the logger.info call that logs db_llm_call.id, job_id,
provider, model to use the function parameter original_provider instead of
provider (i.e., reference original_provider in the message) so the runtime uses
the correct variable and the log prints the provider value.

Comment on lines 22 to 25
SARVAM_API_KEY = os.getenv("SARVAM_API_KEY")
if not SARVAM_API_KEY:
SARVAM_API_KEY = "sk_lmsvfc31_On1bxqwDAqYZoijqBfblr3yf" # for testing only
print("Requested Action: Please set SARVAM_API_KEY , Going ahead with a trail key for testing purposes.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove hardcoded Sarvam API key and stdout print.

Hardcoded secrets can leak and violate security expectations; rely on credential injection instead. If you want a warning, log via logger.warning with a [SarvamAIProvider.__init__] prefix.

🔐 Suggested fix
-SARVAM_API_KEY = os.getenv("SARVAM_API_KEY")
-if not SARVAM_API_KEY:
-    SARVAM_API_KEY = "sk_lmsvfc31_On1bxqwDAqYZoijqBfblr3yf"  # for testing only
-    print("Requested Action: Please set SARVAM_API_KEY , Going ahead with a trail key for testing purposes.")
As per coding guidelines: Prefix all log messages with the function name in square brackets: `logger.info(f"[function_name] Message {mask_string(sensitive_value)}")`.
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/sai.py` around lines 22 - 25, Remove the
hardcoded SARVAM_API_KEY and the print call: stop setting SARVAM_API_KEY to a
fallback secret and remove the stdout print; instead, rely on os.getenv to
provide the key and, if you want a warning in SarvamAIProvider.__init__, call
logger.warning(f"[SarvamAIProvider.__init__] SARVAM_API_KEY not set") (use
mask_string when including any partial secret) rather than printing or embedding
a real key in source; ensure no default secret value remains anywhere in the
module.

Comment on lines +30 to +38
class SarvamAIProvider(BaseProvider):
def __init__(self, client: SarvamAI):
"""Initialize SarvamAI provider with client.

Args:
client: SarvamAI client instance
"""
super().__init__(client)
self.client = client
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "sai.py" -path "*/llm/providers/*"

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 118


🏁 Script executed:

cat -n backend/app/services/llm/providers/sai.py | head -50

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1824


🏁 Script executed:

rg "def __init__" backend/app/services/llm/providers/ -A 3 -B 1

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1468


🏁 Script executed:

cat -n backend/app/services/llm/providers/sai.py | sed -n '40,100p'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 3220


🏁 Script executed:

rg "def " backend/app/services/llm/providers/sai.py -A 1 | head -40

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 497


🏁 Script executed:

cat -n backend/app/services/llm/providers/base.py | head -40

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 1593


🏁 Script executed:

python3 << 'EOF'
# Check if __init__ methods in provider files have return type hints
import re

files_to_check = [
    "backend/app/services/llm/providers/sai.py",
    "backend/app/services/llm/providers/oai.py",
    "backend/app/services/llm/providers/gai.py",
    "backend/app/services/llm/providers/base.py"
]

for filepath in files_to_check:
    try:
        with open(filepath, 'r') as f:
            content = f.read()
            # Find __init__ method
            init_match = re.search(r'def __init__\([^)]*\)(\s*->.*)?:', content)
            if init_match:
                has_return_type = ' -> ' in (init_match.group(1) or '')
                status = "✓ has return type" if has_return_type else "✗ missing return type"
                print(f"{filepath}: {status}")
                print(f"  {init_match.group(0)}")
    except Exception as e:
        print(f"Error reading {filepath}: {e}")
EOF

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 492


Add explicit return type on __init__.

The __init__ method is missing the -> None return type annotation, which is required by the coding guideline for all function parameters and return values.

✍️ Suggested fix
-    def __init__(self, client: SarvamAI):
+    def __init__(self, client: SarvamAI) -> None:
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/sai.py` around lines 30 - 38, The __init__
method in class SarvamAIProvider lacks an explicit return type; update the
SarvamAIProvider.__init__ signature to include the return annotation "-> None"
(i.e., def __init__(self, client: SarvamAI) -> None:) to comply with the
project's function return type guideline, keeping the rest of the body
(super().__init__(client); self.client = client) unchanged.

Comment on lines +95 to +115
# SarvamAI does not provide token usage directly for STT, so we'll use placeholders
# You might estimate based on transcript length or set to 0
input_tokens_estimate = 0 # Not directly provided by SarvamAI STT
output_tokens_estimate = len(sarvam_response.transcript.split()) # Estimate by word count
total_tokens_estimate = input_tokens_estimate + output_tokens_estimate

llm_response = LLMCallResponse(
response=LLMResponse(
provider_response_id=sarvam_response.request_id or "unknown",
conversation_id=None, # SarvamAI STT doesn't have conversation_id
provider=provider_name,
model=model,
output=LLMOutput(text=sarvam_response.transcript or ""),
),
usage=Usage(
input_tokens=input_tokens_estimate,
output_tokens=output_tokens_estimate,
total_tokens=total_tokens_estimate,
reasoning_tokens=None, # Not provided by SarvamAI
),
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing transcript before .split().

If sarvam_response.transcript is None, this raises AttributeError and fails the request.

🐛 Suggested fix
-            input_tokens_estimate = 0 # Not directly provided by SarvamAI STT
-            output_tokens_estimate = len(sarvam_response.transcript.split()) # Estimate by word count
+            transcript = sarvam_response.transcript or ""
+            input_tokens_estimate = 0  # Not directly provided by SarvamAI STT
+            output_tokens_estimate = len(transcript.split())  # Estimate by word count
             total_tokens_estimate = input_tokens_estimate + output_tokens_estimate
@@
-                    output=LLMOutput(text=sarvam_response.transcript or ""),
+                    output=LLMOutput(text=transcript),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# SarvamAI does not provide token usage directly for STT, so we'll use placeholders
# You might estimate based on transcript length or set to 0
input_tokens_estimate = 0 # Not directly provided by SarvamAI STT
output_tokens_estimate = len(sarvam_response.transcript.split()) # Estimate by word count
total_tokens_estimate = input_tokens_estimate + output_tokens_estimate
llm_response = LLMCallResponse(
response=LLMResponse(
provider_response_id=sarvam_response.request_id or "unknown",
conversation_id=None, # SarvamAI STT doesn't have conversation_id
provider=provider_name,
model=model,
output=LLMOutput(text=sarvam_response.transcript or ""),
),
usage=Usage(
input_tokens=input_tokens_estimate,
output_tokens=output_tokens_estimate,
total_tokens=total_tokens_estimate,
reasoning_tokens=None, # Not provided by SarvamAI
),
)
# SarvamAI does not provide token usage directly for STT, so we'll use placeholders
# You might estimate based on transcript length or set to 0
transcript = sarvam_response.transcript or ""
input_tokens_estimate = 0 # Not directly provided by SarvamAI STT
output_tokens_estimate = len(transcript.split()) # Estimate by word count
total_tokens_estimate = input_tokens_estimate + output_tokens_estimate
llm_response = LLMCallResponse(
response=LLMResponse(
provider_response_id=sarvam_response.request_id or "unknown",
conversation_id=None, # SarvamAI STT doesn't have conversation_id
provider=provider_name,
model=model,
output=LLMOutput(text=transcript),
),
usage=Usage(
input_tokens=input_tokens_estimate,
output_tokens=output_tokens_estimate,
total_tokens=total_tokens_estimate,
reasoning_tokens=None, # Not provided by SarvamAI
),
)
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/sai.py` around lines 95 - 115, Guard
against sarvam_response.transcript being None before calling .split(): when
constructing output_tokens_estimate and LLMOutput(text=...), use a safe
transcript variable (e.g., transcript = sarvam_response.transcript or "") or
conditional logic so you never call .split() on None; then compute
output_tokens_estimate = len(transcript.split()) and pass transcript into
LLMOutput and provider_response_id fallback logic in LLMResponse and Usage
creation (LLMCallResponse, LLMResponse, Usage).

Comment on lines +1 to +317
# """
# Unit tests for LLM parameter mapping functions.

from app.models.llm import KaapiLLMParams, KaapiCompletionConfig, NativeCompletionConfig
from app.services.llm.mappers import (
map_kaapi_to_openai_params,
transform_kaapi_config_to_native,
)


class TestMapKaapiToOpenAIParams:
"""Test cases for map_kaapi_to_openai_params function."""

def test_basic_model_mapping(self):
"""Test basic model parameter mapping."""
kaapi_params = KaapiLLMParams(model="gpt-4o")

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result == {"model": "gpt-4o"}
assert warnings == []

def test_instructions_mapping(self):
"""Test instructions parameter mapping."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
instructions="You are a helpful assistant.",
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4"
assert result["instructions"] == "You are a helpful assistant."
assert warnings == []

def test_temperature_mapping(self):
"""Test temperature parameter mapping for non-reasoning models."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
temperature=0.7,
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4"
assert result["temperature"] == 0.7
assert warnings == []

def test_temperature_zero_mapping(self):
"""Test that temperature=0 is correctly mapped (edge case)."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
temperature=0.0,
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["temperature"] == 0.0
assert warnings == []

def test_reasoning_mapping_for_reasoning_models(self):
"""Test reasoning parameter mapping to OpenAI format for reasoning-capable models."""
kaapi_params = KaapiLLMParams(
model="o1",
reasoning="high",
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "o1"
assert result["reasoning"] == {"effort": "high"}
assert warnings == []

def test_knowledge_base_ids_mapping(self):
"""Test knowledge_base_ids mapping to OpenAI tools format."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
knowledge_base_ids=["vs_abc123", "vs_def456"],
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4"
assert "tools" in result
assert len(result["tools"]) == 1
assert result["tools"][0]["type"] == "file_search"
assert result["tools"][0]["vector_store_ids"] == ["vs_abc123", "vs_def456"]
assert result["tools"][0]["max_num_results"] == 20 # default
assert warnings == []

def test_knowledge_base_with_max_num_results(self):
"""Test knowledge_base_ids with custom max_num_results."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
knowledge_base_ids=["vs_abc123"],
max_num_results=50,
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["tools"][0]["max_num_results"] == 50
assert warnings == []

def test_complete_parameter_mapping(self):
"""Test mapping all compatible parameters together."""
kaapi_params = KaapiLLMParams(
model="gpt-4o",
instructions="You are an expert assistant.",
temperature=0.8,
knowledge_base_ids=["vs_123"],
max_num_results=30,
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4o"
assert result["instructions"] == "You are an expert assistant."
assert result["temperature"] == 0.8
assert result["tools"][0]["type"] == "file_search"
assert result["tools"][0]["vector_store_ids"] == ["vs_123"]
assert result["tools"][0]["max_num_results"] == 30
assert warnings == []

def test_reasoning_suppressed_for_non_reasoning_models(self):
"""Test that reasoning is suppressed with warning for non-reasoning models."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
reasoning="high",
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4"
assert "reasoning" not in result
assert len(warnings) == 1
assert "reasoning" in warnings[0].lower()
assert "does not support reasoning" in warnings[0]

def test_temperature_suppressed_for_reasoning_models(self):
"""Test that temperature is suppressed with warning for reasoning models when reasoning is set."""
kaapi_params = KaapiLLMParams(
model="o1",
temperature=0.7,
reasoning="high",
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "o1"
assert result["reasoning"] == {"effort": "high"}
assert "temperature" not in result
assert len(warnings) == 1
assert "temperature" in warnings[0].lower()
assert "suppressed" in warnings[0]

def test_temperature_without_reasoning_for_reasoning_models(self):
"""Test that temperature is suppressed for reasoning models even without explicit reasoning parameter."""
kaapi_params = KaapiLLMParams(
model="o1",
temperature=0.7,
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "o1"
assert "temperature" not in result
assert "reasoning" not in result
assert len(warnings) == 1
assert "temperature" in warnings[0].lower()
assert "suppressed" in warnings[0]

def test_minimal_params(self):
"""Test mapping with minimal parameters (only model)."""
kaapi_params = KaapiLLMParams(model="gpt-4")

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result == {"model": "gpt-4"}
assert warnings == []

def test_only_knowledge_base_ids(self):
"""Test mapping with only knowledge_base_ids and model."""
kaapi_params = KaapiLLMParams(
model="gpt-4",
knowledge_base_ids=["vs_xyz"],
)

result, warnings = map_kaapi_to_openai_params(kaapi_params)

assert result["model"] == "gpt-4"
assert "tools" in result
assert result["tools"][0]["vector_store_ids"] == ["vs_xyz"]
assert warnings == []


class TestTransformKaapiConfigToNative:
"""Test cases for transform_kaapi_config_to_native function."""

def test_transform_openai_config(self):
"""Test transformation of Kaapi OpenAI config to native format."""
kaapi_config = KaapiCompletionConfig(
provider="openai",
params=KaapiLLMParams(
model="gpt-4",
temperature=0.7,
),
)

result, warnings = transform_kaapi_config_to_native(kaapi_config)

assert isinstance(result, NativeCompletionConfig)
assert result.provider == "openai-native"
assert result.params["model"] == "gpt-4"
assert result.params["temperature"] == 0.7
assert warnings == []

def test_transform_with_all_params(self):
"""Test transformation with all Kaapi parameters."""
kaapi_config = KaapiCompletionConfig(
provider="openai",
params=KaapiLLMParams(
model="gpt-4o",
instructions="System prompt here",
temperature=0.5,
knowledge_base_ids=["vs_abc"],
max_num_results=25,
),
)

result, warnings = transform_kaapi_config_to_native(kaapi_config)

assert result.provider == "openai-native"
assert result.params["model"] == "gpt-4o"
assert result.params["instructions"] == "System prompt here"
assert result.params["temperature"] == 0.5
assert result.params["tools"][0]["type"] == "file_search"
assert result.params["tools"][0]["max_num_results"] == 25
assert warnings == []

def test_transform_with_reasoning(self):
"""Test transformation with reasoning parameter for reasoning-capable models."""
kaapi_config = KaapiCompletionConfig(
provider="openai",
params=KaapiLLMParams(
model="o1",
reasoning="medium",
),
)

result, warnings = transform_kaapi_config_to_native(kaapi_config)

assert result.provider == "openai-native"
assert result.params["model"] == "o1"
assert result.params["reasoning"] == {"effort": "medium"}
assert warnings == []

def test_transform_with_both_temperature_and_reasoning(self):
"""Test that transformation handles temperature + reasoning intelligently for reasoning models."""
kaapi_config = KaapiCompletionConfig(
provider="openai",
params=KaapiLLMParams(
model="o1",
temperature=0.7,
reasoning="high",
),
)

result, warnings = transform_kaapi_config_to_native(kaapi_config)

assert result.provider == "openai-native"
assert result.params["model"] == "o1"
assert result.params["reasoning"] == {"effort": "high"}
assert "temperature" not in result.params
assert len(warnings) == 1
assert "temperature" in warnings[0].lower()
assert "suppressed" in warnings[0]

def test_unsupported_provider_raises_error(self):
"""Test that unsupported providers raise ValueError."""
# Note: This would require modifying KaapiCompletionConfig to accept other providers
# For now, this tests the error handling in the mapper
# We'll create a mock config that bypasses validation
from unittest.mock import MagicMock

mock_config = MagicMock()
mock_config.provider = "unsupported-provider"
mock_config.params = KaapiLLMParams(model="some-model")

with pytest.raises(ValueError) as exc_info:
transform_kaapi_config_to_native(mock_config)

assert "Unsupported provider" in str(exc_info.value)

def test_transform_preserves_param_structure(self):
"""Test that transformation correctly structures nested parameters."""
kaapi_config = KaapiCompletionConfig(
provider="openai",
params=KaapiLLMParams(
model="gpt-4",
knowledge_base_ids=["vs_1", "vs_2", "vs_3"],
max_num_results=15,
),
)

result, warnings = transform_kaapi_config_to_native(kaapi_config)

# Verify the nested structure is correct
assert isinstance(result.params["tools"], list)
assert isinstance(result.params["tools"][0], dict)
assert isinstance(result.params["tools"][0]["vector_store_ids"], list)
assert len(result.params["tools"][0]["vector_store_ids"]) == 3
assert warnings == []
# Tests the transformation of Kaapi-abstracted parameters to provider-native formats.
# """

# import pytest

# from app.models.llm import KaapiLLMParams, KaapiCompletionConfig, NativeCompletionConfig
# from app.services.llm.mappers import (
# map_kaapi_to_openai_params,
# transform_kaapi_config_to_native,
# )


# class TestMapKaapiToOpenAIParams:
# """Test cases for map_kaapi_to_openai_params function."""

# def test_basic_model_mapping(self):
# """Test basic model parameter mapping."""
# kaapi_params = KaapiLLMParams(model="gpt-4o")

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result == {"model": "gpt-4o"}
# assert warnings == []

# def test_instructions_mapping(self):
# """Test instructions parameter mapping."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# instructions="You are a helpful assistant.",
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4"
# assert result["instructions"] == "You are a helpful assistant."
# assert warnings == []

# def test_temperature_mapping(self):
# """Test temperature parameter mapping for non-reasoning models."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# temperature=0.7,
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4"
# assert result["temperature"] == 0.7
# assert warnings == []

# def test_temperature_zero_mapping(self):
# """Test that temperature=0 is correctly mapped (edge case)."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# temperature=0.0,
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["temperature"] == 0.0
# assert warnings == []

# def test_reasoning_mapping_for_reasoning_models(self):
# """Test reasoning parameter mapping to OpenAI format for reasoning-capable models."""
# kaapi_params = KaapiLLMParams(
# model="o1",
# reasoning="high",
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "o1"
# assert result["reasoning"] == {"effort": "high"}
# assert warnings == []

# def test_knowledge_base_ids_mapping(self):
# """Test knowledge_base_ids mapping to OpenAI tools format."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# knowledge_base_ids=["vs_abc123", "vs_def456"],
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4"
# assert "tools" in result
# assert len(result["tools"]) == 1
# assert result["tools"][0]["type"] == "file_search"
# assert result["tools"][0]["vector_store_ids"] == ["vs_abc123", "vs_def456"]
# assert result["tools"][0]["max_num_results"] == 20 # default
# assert warnings == []

# def test_knowledge_base_with_max_num_results(self):
# """Test knowledge_base_ids with custom max_num_results."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# knowledge_base_ids=["vs_abc123"],
# max_num_results=50,
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["tools"][0]["max_num_results"] == 50
# assert warnings == []

# def test_complete_parameter_mapping(self):
# """Test mapping all compatible parameters together."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4o",
# instructions="You are an expert assistant.",
# temperature=0.8,
# knowledge_base_ids=["vs_123"],
# max_num_results=30,
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4o"
# assert result["instructions"] == "You are an expert assistant."
# assert result["temperature"] == 0.8
# assert result["tools"][0]["type"] == "file_search"
# assert result["tools"][0]["vector_store_ids"] == ["vs_123"]
# assert result["tools"][0]["max_num_results"] == 30
# assert warnings == []

# def test_reasoning_suppressed_for_non_reasoning_models(self):
# """Test that reasoning is suppressed with warning for non-reasoning models."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# reasoning="high",
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4"
# assert "reasoning" not in result
# assert len(warnings) == 1
# assert "reasoning" in warnings[0].lower()
# assert "does not support reasoning" in warnings[0]

# def test_temperature_suppressed_for_reasoning_models(self):
# """Test that temperature is suppressed with warning for reasoning models when reasoning is set."""
# kaapi_params = KaapiLLMParams(
# model="o1",
# temperature=0.7,
# reasoning="high",
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "o1"
# assert result["reasoning"] == {"effort": "high"}
# assert "temperature" not in result
# assert len(warnings) == 1
# assert "temperature" in warnings[0].lower()
# assert "suppressed" in warnings[0]

# def test_temperature_without_reasoning_for_reasoning_models(self):
# """Test that temperature is suppressed for reasoning models even without explicit reasoning parameter."""
# kaapi_params = KaapiLLMParams(
# model="o1",
# temperature=0.7,
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "o1"
# assert "temperature" not in result
# assert "reasoning" not in result
# assert len(warnings) == 1
# assert "temperature" in warnings[0].lower()
# assert "suppressed" in warnings[0]

# def test_minimal_params(self):
# """Test mapping with minimal parameters (only model)."""
# kaapi_params = KaapiLLMParams(model="gpt-4")

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result == {"model": "gpt-4"}
# assert warnings == []

# def test_only_knowledge_base_ids(self):
# """Test mapping with only knowledge_base_ids and model."""
# kaapi_params = KaapiLLMParams(
# model="gpt-4",
# knowledge_base_ids=["vs_xyz"],
# )

# result, warnings = map_kaapi_to_openai_params(kaapi_params)

# assert result["model"] == "gpt-4"
# assert "tools" in result
# assert result["tools"][0]["vector_store_ids"] == ["vs_xyz"]
# assert warnings == []


# class TestTransformKaapiConfigToNative:
# """Test cases for transform_kaapi_config_to_native function."""

# def test_transform_openai_config(self):
# """Test transformation of Kaapi OpenAI config to native format."""
# kaapi_config = KaapiCompletionConfig(
# provider="openai",
# params=KaapiLLMParams(
# model="gpt-4",
# temperature=0.7,
# ),
# )

# result, warnings = transform_kaapi_config_to_native(kaapi_config)

# assert isinstance(result, NativeCompletionConfig)
# assert result.provider == "openai-native"
# assert result.params["model"] == "gpt-4"
# assert result.params["temperature"] == 0.7
# assert warnings == []

# def test_transform_with_all_params(self):
# """Test transformation with all Kaapi parameters."""
# kaapi_config = KaapiCompletionConfig(
# provider="openai",
# params=KaapiLLMParams(
# model="gpt-4o",
# instructions="System prompt here",
# temperature=0.5,
# knowledge_base_ids=["vs_abc"],
# max_num_results=25,
# ),
# )

# result, warnings = transform_kaapi_config_to_native(kaapi_config)

# assert result.provider == "openai-native"
# assert result.params["model"] == "gpt-4o"
# assert result.params["instructions"] == "System prompt here"
# assert result.params["temperature"] == 0.5
# assert result.params["tools"][0]["type"] == "file_search"
# assert result.params["tools"][0]["max_num_results"] == 25
# assert warnings == []

# def test_transform_with_reasoning(self):
# """Test transformation with reasoning parameter for reasoning-capable models."""
# kaapi_config = KaapiCompletionConfig(
# provider="openai",
# params=KaapiLLMParams(
# model="o1",
# reasoning="medium",
# ),
# )

# result, warnings = transform_kaapi_config_to_native(kaapi_config)

# assert result.provider == "openai-native"
# assert result.params["model"] == "o1"
# assert result.params["reasoning"] == {"effort": "medium"}
# assert warnings == []

# def test_transform_with_both_temperature_and_reasoning(self):
# """Test that transformation handles temperature + reasoning intelligently for reasoning models."""
# kaapi_config = KaapiCompletionConfig(
# provider="openai",
# params=KaapiLLMParams(
# model="o1",
# temperature=0.7,
# reasoning="high",
# ),
# )

# result, warnings = transform_kaapi_config_to_native(kaapi_config)

# assert result.provider == "openai-native"
# assert result.params["model"] == "o1"
# assert result.params["reasoning"] == {"effort": "high"}
# assert "temperature" not in result.params
# assert len(warnings) == 1
# assert "temperature" in warnings[0].lower()
# assert "suppressed" in warnings[0]

# def test_unsupported_provider_raises_error(self):
# """Test that unsupported providers raise ValueError."""
# # Note: This would require modifying KaapiCompletionConfig to accept other providers
# # For now, this tests the error handling in the mapper
# # We'll create a mock config that bypasses validation
# from unittest.mock import MagicMock

# mock_config = MagicMock()
# mock_config.provider = "unsupported-provider"
# mock_config.params = KaapiLLMParams(model="some-model")

# with pytest.raises(ValueError) as exc_info:
# transform_kaapi_config_to_native(mock_config)

# assert "Unsupported provider" in str(exc_info.value)

# def test_transform_preserves_param_structure(self):
# """Test that transformation correctly structures nested parameters."""
# kaapi_config = KaapiCompletionConfig(
# provider="openai",
# params=KaapiLLMParams(
# model="gpt-4",
# knowledge_base_ids=["vs_1", "vs_2", "vs_3"],
# max_num_results=15,
# ),
# )

# result, warnings = transform_kaapi_config_to_native(kaapi_config)

# # Verify the nested structure is correct
# assert isinstance(result.params["tools"], list)
# assert isinstance(result.params["tools"][0], dict)
# assert isinstance(result.params["tools"][0]["vector_store_ids"], list)
# assert len(result.params["tools"][0]["vector_store_ids"]) == 3
# assert warnings == []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tests are fully commented out, disabling coverage.
Commenting out the entire module removes these checks from the suite and hides regressions. Please restore the tests or replace them with explicit skips (e.g., pytest.mark.skip) plus a tracked follow‑up.

If you want, I can help re-enable or update these tests for the new mapper behavior.

🤖 Prompt for AI Agents
In `@backend/app/tests/services/llm/test_mappers.py` around lines 1 - 317, The
test module has been entirely commented out which removes coverage; restore the
tests by uncommenting the file and its imports and test classes (references:
KaapiLLMParams, KaapiCompletionConfig, NativeCompletionConfig,
map_kaapi_to_openai_params, transform_kaapi_config_to_native) so they run
normally, or if intentionally disabled mark individual tests with
pytest.mark.skip and add a TODO/link to a tracked issue; ensure pytest is
imported and any mocked tests (e.g., using MagicMock) remain valid after
re-enabling so the CI regains these checks.

Comment on lines +342 to +343
config_type = completion_config.get("type")
provider = completion_config.get("provider", "openai-native")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing default for config_type could cause validation errors.

The type field is mandatory in both NativeCompletionConfig and KaapiCompletionConfig with valid values "text", "stt", or "tts". If the previous version's config_blob lacks a type key, config_type will be None, causing Pydantic validation to fail on lines 350 and 365.

🛡️ Proposed fix to add a default value
-            config_type = completion_config.get("type")
+            config_type = completion_config.get("type", "text")

Based on learnings: the type field in config_blob.completion is compulsory/mandatory during config creation with valid values "text", "stt", or "tts".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
config_type = completion_config.get("type")
provider = completion_config.get("provider", "openai-native")
config_type = completion_config.get("type", "text")
provider = completion_config.get("provider", "openai-native")
🤖 Prompt for AI Agents
In `@backend/app/tests/utils/test_data.py` around lines 342 - 343, The code reads
completion_config.get("type") into config_type without a default which can lead
to Pydantic validation errors for NativeCompletionConfig/KaapiCompletionConfig;
set a safe default (e.g., "text") when reading the field so config_type =
completion_config.get("type", "text") (or equivalent) before passing into
NativeCompletionConfig/KaapiCompletionConfig construction to ensure the required
"type" is always present and one of "text"/"stt"/"tts".

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@backend/app/services/llm/providers/registry.py`:
- Around line 1-2: Remove the module-level dotenv side-effect: delete the "from
dotenv import load_dotenv" import and the module-level call to load_dotenv() in
registry.py (the top-level import block and the stray load_dotenv() call),
leaving environment loading to application bootstrap or tests; also verify no
other code in the file relies on load_dotenv() so you don't introduce
missing-config issues.
- Line 96: The logger.error call in get_llm_provider is missing the required
function-name prefix; update the log message in the exception handler where
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
is called so it starts with "[get_llm_provider]" (e.g., "[get_llm_provider]
Failed to initialize {provider_type} client: {e}") ensuring exc_info=True
remains set and no other behavior changes.
- Around line 14-25: Remove the ad hoc testing code and its unused imports: drop
GenerateContentConfig, NativeCompletionConfig, LLMCallResponse, QueryParams,
LLMOutput, LLMResponse, and Usage from the top of registry.py and delete the
entire if __name__ == "__main__": testing block that references them; ensure
only production imports remain and run tests/lint to confirm no remaining
references to those symbols (look for any usage inside functions or the removed
block before committing).
🧹 Nitpick comments (1)
backend/app/services/llm/providers/sai.py (1)

40-44: Consider a more specific return type for create_client.

The return type Any is imprecise. Since this method returns a SarvamAI client instance, the return type should reflect that.

Proposed fix
     `@staticmethod`
-    def create_client(credentials: dict[str, Any]) -> Any:
+    def create_client(credentials: dict[str, Any]) -> SarvamAI:
         if "api_key" not in credentials:
             raise ValueError("API Key for SarvamAI Not Set")
         return SarvamAI(api_subscription_key=credentials["api_key"])

Comment on lines +1 to +2
import os
from dotenv import load_dotenv
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove dotenv import and load_dotenv() call from production module.

The load_dotenv() call at module level (line 27) causes a side effect on every import, which is inappropriate for production code. This appears to be leftover from the ad hoc testing block.

Proposed fix
-import os
-from dotenv import load_dotenv
 import logging

And remove line 27:

-load_dotenv()

Also applies to: 27-27

🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/registry.py` around lines 1 - 2, Remove
the module-level dotenv side-effect: delete the "from dotenv import load_dotenv"
import and the module-level call to load_dotenv() in registry.py (the top-level
import block and the stray load_dotenv() call), leaving environment loading to
application bootstrap or tests; also verify no other code in the file relies on
load_dotenv() so you don't introduce missing-config issues.

Comment on lines +14 to +25
from google.genai.types import GenerateContentConfig

# temporary import

from app.models.llm import (
NativeCompletionConfig,
LLMCallResponse,
QueryParams,
LLMOutput,
LLMResponse,
Usage,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused imports tied to ad hoc testing code.

These imports (GenerateContentConfig, NativeCompletionConfig, LLMCallResponse, QueryParams, LLMOutput, LLMResponse, Usage) are only used in the if __name__ == "__main__": block. They should be removed along with the testing code.

Proposed fix
-from google.genai.types import GenerateContentConfig
-
-# temporary import
-
-from app.models.llm import (
-    NativeCompletionConfig,
-    LLMCallResponse,
-    QueryParams,
-    LLMOutput,
-    LLMResponse,
-    Usage,
-)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from google.genai.types import GenerateContentConfig
# temporary import
from app.models.llm import (
NativeCompletionConfig,
LLMCallResponse,
QueryParams,
LLMOutput,
LLMResponse,
Usage,
)
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/registry.py` around lines 14 - 25, Remove
the ad hoc testing code and its unused imports: drop GenerateContentConfig,
NativeCompletionConfig, LLMCallResponse, QueryParams, LLMOutput, LLMResponse,
and Usage from the top of registry.py and delete the entire if __name__ ==
"__main__": testing block that references them; ensure only production imports
remain and run tests/lint to confirm no remaining references to those symbols
(look for any usage inside functions or the removed block before committing).

# Re-raise ValueError for credential/configuration errors
raise
except Exception as e:
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add function name prefix to log message.

The log message is missing the required [get_llm_provider] prefix per coding guidelines.

Proposed fix
-        logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
+        logger.error(f"[get_llm_provider] Failed to initialize {provider_type} client: {e}", exc_info=True)

As per coding guidelines: Prefix all log messages with the function name in square brackets.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
logger.error(f"[get_llm_provider] Failed to initialize {provider_type} client: {e}", exc_info=True)
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/registry.py` at line 96, The logger.error
call in get_llm_provider is missing the required function-name prefix; update
the log message in the exception handler where logger.error(f"Failed to
initialize {provider_type} client: {e}", exc_info=True) is called so it starts
with "[get_llm_provider]" (e.g., "[get_llm_provider] Failed to initialize
{provider_type} client: {e}") ensuring exc_info=True remains set and no other
behavior changes.

@vprashrex vprashrex moved this to In Progress in Kaapi-dev Feb 4, 2026
@vprashrex vprashrex linked an issue Feb 4, 2026 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@backend/app/services/llm/providers/sai.py`:
- Around line 120-127: The logs in the execute_stt function are being prefixed
with provider_name instead of the required function name; update the logger
calls in execute_stt (e.g., the info log using sarvam_response.request_id and
the error log using error_message) to use the function name in square brackets
(e.g., "[execute_stt]") as the prefix, and ensure any sensitive values are
wrapped with mask_string(...) per guidelines; change both logger.info and
logger.error invocations in the execute_stt function accordingly.
- Line 5: The module imports the external package name sarvamai (see the import
"from sarvamai import SarvamAI" in sai.py) but it is not declared in project
dependencies; add "sarvamai" to your dependency manifest (e.g., pyproject.toml
[tool.poetry.dependencies] or requirements.txt) with an appropriate version
constraint, update the lockfile (poetry lock / pip-compile or pip freeze) and
run the installer (poetry install or pip install -r requirements.txt) to ensure
the package is available at runtime.

In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_GeminiProvider.py`:
- Around line 67-99: The function get_llm_provider has a duplicated assignment
of credential_provider (remove the extra credential_provider =
provider_type.replace("-native", "")) and the error log should be prefixed with
the function name in square brackets; update the except Exception block to call
logger.error(f"[get_llm_provider] Failed to initialize {provider_type} client:
{mask_string(str(e))}", exc_info=True) (use mask_string for sensitive info)
while keeping the existing re-raise of ValueError and the RuntimeError raise
behavior.
- Around line 104-112: The test currently prints the raw GEMINI_API_KEY in the
__main__ block (variable GEMINI_API_KEY) which leaks secrets; remove the print
that outputs the full key and instead either omit printing it entirely or print
a masked version (e.g., only show a fixed prefix/suffix or replace with
asterisks) when checking presence, and keep the existing presence check/exit(1)
logic intact. Locate the prints in the main guard (if __name__ == "__main__")
and update the output statements to avoid exposing the secret.
- Around line 9-12: The import for GoogleAIProvider is pointing to the wrong
module; update the import statement that references GoogleAIProvider so it
imports from app.services.llm.providers.gai instead of
app.services.llm.providers.gai2 (replace the import of GoogleAIProvider
accordingly to reference the correct module name `gai`).

In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_SarvamProvider.py`:
- Around line 1-22: The file has duplicate imports of os and tempfile (they
appear twice at top and again near the bottom), causing Ruff F811; remove the
redundant imports so only a single import line for os and for tempfile remains
(keep the earliest/top-level imports and delete the later ad-hoc duplicates),
then run the linter to confirm F811 is resolved—look for the duplicate import
statements in test_STT_SarvamProvider.py to locate and remove the extra lines.
- Around line 73-104: Remove the duplicate assignment to credential_provider in
get_llm_provider (keep a single credential_provider =
provider_type.replace("-native", "")), and update the error log call
(logger.error) to include the function name prefix in square brackets and mask
sensitive values; e.g., change the message to include "[get_llm_provider]" at
the start and apply mask_string(...) to any sensitive variable you include (for
example mask_string(credentials) or mask_string(provider_type)) while keeping
exc_info=True.
- Around line 108-119: Remove the hardcoded Sarvam API key in the __main__ test
block: replace the literal assignment to SARVAM_API_KEY and construction of
mock_credentials with loading from a safe source (e.g.,
os.environ.get("SARVAM_API_KEY")) or a test fixture/mocked secret provider;
ensure the code validates presence and exits if missing (preserve the existing
if not SARVAM_API_KEY check) and update mock_credentials to use the sourced
value (mock_credentials = {"api_key": SARVAM_API_KEY}) so no secrets are
committed in test_STT_SarvamProvider.py.
🧹 Nitpick comments (1)
backend/app/models/llm/request.py (1)

376-459: Use sa_column_kwargs["comment"] for column comments.

Several fields embed comments inside sa.Column; the guideline prefers sa_column_kwargs for status/type/JSON metadata fields. As per coding guidelines: backend/app/models/**/*.py: Use sa_column_kwargs["comment"] to describe database columns, especially for non-obvious purposes, status/type fields, JSON/metadata columns, and foreign keys.

🛠️ Example fix (apply similarly to other fields)
-    input_type: Literal["text", "audio", "image"] = Field(
-        ...,
-        sa_column=sa.Column(
-            sa.String,
-            nullable=False,
-            comment="Input type: text, audio, image",
-        ),
-    )
+    input_type: Literal["text", "audio", "image"] = Field(
+        ...,
+        sa_column=sa.Column(sa.String, nullable=False),
+        sa_column_kwargs={"comment": "Input type: text, audio, image"},
+    )

import os
from typing import Any

from sarvamai import SarvamAI
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check dependency manifests for sarvamai
rg -n "sarvamai" -g 'pyproject.toml' -g 'requirements*.txt' -g 'Pipfile*' -g 'setup.cfg' -g 'setup.py'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 57


Add sarvamai to the project's dependencies.

The sarvamai package is imported on line 5 but is not declared in any dependency manifest (pyproject.toml, requirements.txt, setup.py, etc.). This import will fail at runtime, preventing the module from being loaded. Add the package to your project's dependency declarations.

🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/sai.py` at line 5, The module imports the
external package name sarvamai (see the import "from sarvamai import SarvamAI"
in sai.py) but it is not declared in project dependencies; add "sarvamai" to
your dependency manifest (e.g., pyproject.toml [tool.poetry.dependencies] or
requirements.txt) with an appropriate version constraint, update the lockfile
(poetry lock / pip-compile or pip freeze) and run the installer (poetry install
or pip install -r requirements.txt) to ensure the package is available at
runtime.

Comment on lines +120 to +127
logger.info(
f"[{provider_name}.execute_stt] Successfully transcribed audio: {sarvam_response.request_id}"
)
return llm_response, None

except Exception as e:
error_message = f"SarvamAI STT transcription failed: {str(e)}"
logger.error(f"[{provider_name}.execute_stt] {error_message}", exc_info=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prefix STT logs with the function name.

The guideline calls for the function name in square brackets; the current prefix uses the provider name instead. As per coding guidelines: Prefix all log messages with the function name in square brackets: logger.info(f"[function_name] Message {mask_string(sensitive_value)}").

🛠️ Suggested fix
-            logger.info(
-                f"[{provider_name}.execute_stt] Successfully transcribed audio: {sarvam_response.request_id}"
-            )
+            logger.info(
+                f"[SarvamAIProvider._execute_stt] Successfully transcribed audio: {sarvam_response.request_id}"
+            )
@@
-            logger.error(f"[{provider_name}.execute_stt] {error_message}", exc_info=True)
+            logger.error(
+                f"[SarvamAIProvider._execute_stt] {error_message}", exc_info=True
+            )
🤖 Prompt for AI Agents
In `@backend/app/services/llm/providers/sai.py` around lines 120 - 127, The logs
in the execute_stt function are being prefixed with provider_name instead of the
required function name; update the logger calls in execute_stt (e.g., the info
log using sarvam_response.request_id and the error log using error_message) to
use the function name in square brackets (e.g., "[execute_stt]") as the prefix,
and ensure any sensitive values are wrapped with mask_string(...) per
guidelines; change both logger.info and logger.error invocations in the
execute_stt function accordingly.

Comment on lines +9 to +12
from app.services.llm.providers.base import BaseProvider
from app.services.llm.providers.oai import OpenAIProvider
from app.services.llm.providers.gai2 import GoogleAIProvider
from app.tests.services.llm.providers.STTproviders.test_data_speechsamples import mydata
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate GoogleAIProvider implementation and confirm module path
fd -g 'gai2.py' -g 'gai.py' backend/app/services/llm/providers
rg -n "class GoogleAIProvider" backend/app/services/llm/providers -g '*.py'

Repository: ProjectTech4DevAI/kaapi-backend

Length of output: 227


Correct the GoogleAIProvider import path from gai2 to gai.

The GoogleAIProvider class is defined in backend/app/services/llm/providers/gai.py, not gai2.py. Change the import on line 11 from from app.services.llm.providers.gai2 import GoogleAIProvider to from app.services.llm.providers.gai import GoogleAIProvider.

🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_GeminiProvider.py`
around lines 9 - 12, The import for GoogleAIProvider is pointing to the wrong
module; update the import statement that references GoogleAIProvider so it
imports from app.services.llm.providers.gai instead of
app.services.llm.providers.gai2 (replace the import of GoogleAIProvider
accordingly to reference the correct module name `gai`).

Comment on lines +67 to +99
def get_llm_provider(
session: Session, provider_type: str, project_id: int, organization_id: int
) -> BaseProvider:
provider_class = LLMProvider.get_provider_class(provider_type)

# e.g "openai-native" -> "openai", "claude-native" -> "claude"
credential_provider = provider_type.replace("-native", "")

# e.g., "openai-native" → "openai", "claude-native" → "claude"
credential_provider = provider_type.replace("-native", "")

credentials = get_provider_credential(
session=session,
provider=credential_provider,
project_id=project_id,
org_id=organization_id,
)

if not credentials:
raise ValueError(
f"Credentials for provider '{credential_provider}' not configured for this project."
)

try:
client = provider_class.create_client(credentials=credentials)
return provider_class(client=client)
except ValueError:
# Re-raise ValueError for credential/configuration errors
raise
except Exception as e:
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
raise RuntimeError(f"Could not connect to {provider_type} services.")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deduplicate credential_provider assignment and fix log prefix.

There’s a repeated assignment, and the error log should include the function name prefix. As per coding guidelines: Prefix all log messages with the function name in square brackets: logger.info(f"[function_name] Message {mask_string(sensitive_value)}").

🛠️ Suggested fix
-    # e.g "openai-native" -> "openai", "claude-native" -> "claude"
-    credential_provider = provider_type.replace("-native", "")
-
-    # e.g., "openai-native" → "openai", "claude-native" → "claude"
-    credential_provider = provider_type.replace("-native", "")
+    # e.g., "openai-native" → "openai", "claude-native" → "claude"
+    credential_provider = provider_type.replace("-native", "")
@@
-        logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
+        logger.error(
+            f"[get_llm_provider] Failed to initialize {provider_type} client: {e}",
+            exc_info=True,
+        )
🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_GeminiProvider.py`
around lines 67 - 99, The function get_llm_provider has a duplicated assignment
of credential_provider (remove the extra credential_provider =
provider_type.replace("-native", "")) and the error log should be prefixed with
the function name in square brackets; update the except Exception block to call
logger.error(f"[get_llm_provider] Failed to initialize {provider_type} client:
{mask_string(str(e))}", exc_info=True) (use mask_string for sensitive info)
while keeping the existing re-raise of ValueError and the RuntimeError raise
behavior.

Comment on lines +104 to +112
if __name__ == "__main__":
print("Hello N, Starting GoogleAIProvider STT test...")
# 1. Simulate environment/credentials
# GEMINI_KEY is already defined as GEMINI_API_KEY in the notebook
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
print(f"GEMINI_API_KEY: {GEMINI_API_KEY}")
if not GEMINI_API_KEY:
print("GEMINI_API_KEY is not set.")
exit(1)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid printing the API key to stdout.

This leaks secrets in logs; remove the print or mask the value.

🔐 Suggested fix
-    print(f"GEMINI_API_KEY: {GEMINI_API_KEY}")
🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_GeminiProvider.py`
around lines 104 - 112, The test currently prints the raw GEMINI_API_KEY in the
__main__ block (variable GEMINI_API_KEY) which leaks secrets; remove the print
that outputs the full key and instead either omit printing it entirely or print
a masked version (e.g., only show a fixed prefix/suffix or replace with
asterisks) when checking presence, and keep the existing presence check/exit(1)
logic intact. Locate the prints in the main guard (if __name__ == "__main__")
and update the output statements to avoid exposing the secret.

Comment on lines +1 to +22
import os
from dotenv import load_dotenv
import logging

from sqlmodel import Session
from openai import OpenAI

from app.crud import get_provider_credential
from app.services.llm.providers.base import BaseProvider
from app.services.llm.providers.oai import OpenAIProvider
from app.services.llm.providers.gai2 import GoogleAIProvider
from app.services.llm.providers.sai import SarvamAIProvider

from app.tests.services.llm.providers.STTproviders.test_data_speechsamples import mydata

import tempfile


# ad hoc testing code for SarvamAIProvider
import os
import tempfile

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate os/tempfile imports.

They’re redefined later in the file and trip Ruff F811.

🧹 Suggested fix
-# ad hoc testing code for SarvamAIProvider
-import os
-import tempfile
+# ad hoc testing code for SarvamAIProvider
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import os
from dotenv import load_dotenv
import logging
from sqlmodel import Session
from openai import OpenAI
from app.crud import get_provider_credential
from app.services.llm.providers.base import BaseProvider
from app.services.llm.providers.oai import OpenAIProvider
from app.services.llm.providers.gai2 import GoogleAIProvider
from app.services.llm.providers.sai import SarvamAIProvider
from app.tests.services.llm.providers.STTproviders.test_data_speechsamples import mydata
import tempfile
# ad hoc testing code for SarvamAIProvider
import os
import tempfile
import os
from dotenv import load_dotenv
import logging
from sqlmodel import Session
from openai import OpenAI
from app.crud import get_provider_credential
from app.services.llm.providers.base import BaseProvider
from app.services.llm.providers.oai import OpenAIProvider
from app.services.llm.providers.gai2 import GoogleAIProvider
from app.services.llm.providers.sai import SarvamAIProvider
from app.tests.services.llm.providers.STTproviders.test_data_speechsamples import mydata
import tempfile
# ad hoc testing code for SarvamAIProvider
🧰 Tools
🪛 Ruff (0.14.14)

[error] 20-20: Redefinition of unused os from line 1: os redefined here

Remove definition: os

(F811)


[error] 21-21: Redefinition of unused tempfile from line 16: tempfile redefined here

Remove definition: tempfile

(F811)

🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_SarvamProvider.py`
around lines 1 - 22, The file has duplicate imports of os and tempfile (they
appear twice at top and again near the bottom), causing Ruff F811; remove the
redundant imports so only a single import line for os and for tempfile remains
(keep the earliest/top-level imports and delete the later ad-hoc duplicates),
then run the linter to confirm F811 is resolved—look for the duplicate import
statements in test_STT_SarvamProvider.py to locate and remove the extra lines.

Comment on lines +73 to +104
def get_llm_provider(
session: Session, provider_type: str, project_id: int, organization_id: int
) -> BaseProvider:
provider_class = LLMProvider.get_provider_class(provider_type)

# e.g "openai-native" -> "openai", "claude-native" -> "claude"
credential_provider = provider_type.replace("-native", "")

# e.g., "openai-native" → "openai", "claude-native" → "claude"
credential_provider = provider_type.replace("-native", "")

credentials = get_provider_credential(
session=session,
provider=credential_provider,
project_id=project_id,
org_id=organization_id,
)

if not credentials:
raise ValueError(
f"Credentials for provider '{credential_provider}' not configured for this project."
)

try:
client = provider_class.create_client(credentials=credentials)
return provider_class(client=client)
except ValueError:
# Re-raise ValueError for credential/configuration errors
raise
except Exception as e:
logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
raise RuntimeError(f"Could not connect to {provider_type} services.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deduplicate credential_provider assignment and fix log prefix.

There’s a repeated assignment, and the log message should include the function name prefix. As per coding guidelines: Prefix all log messages with the function name in square brackets: logger.info(f"[function_name] Message {mask_string(sensitive_value)}").

🛠️ Suggested fix
-    # e.g "openai-native" -> "openai", "claude-native" -> "claude"
-    credential_provider = provider_type.replace("-native", "")
-
-    # e.g., "openai-native" → "openai", "claude-native" → "claude"
-    credential_provider = provider_type.replace("-native", "")
+    # e.g., "openai-native" → "openai", "claude-native" → "claude"
+    credential_provider = provider_type.replace("-native", "")
@@
-        logger.error(f"Failed to initialize {provider_type} client: {e}", exc_info=True)
+        logger.error(
+            f"[get_llm_provider] Failed to initialize {provider_type} client: {e}",
+            exc_info=True,
+        )
🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_SarvamProvider.py`
around lines 73 - 104, Remove the duplicate assignment to credential_provider in
get_llm_provider (keep a single credential_provider =
provider_type.replace("-native", "")), and update the error log call
(logger.error) to include the function name prefix in square brackets and mask
sensitive values; e.g., change the message to include "[get_llm_provider]" at
the start and apply mask_string(...) to any sensitive variable you include (for
example mask_string(credentials) or mask_string(provider_type)) while keeping
exc_info=True.

Comment on lines +108 to +119
if __name__ == "__main__":
# 1. Simulate environment/credentials
# SARVAM_API_KEY is already defined in the notebook
SARVAM_API_KEY = "sk_lmsvfc31_On1bxqwDAqYZoijqBfblr3yf" # for testing only

if not SARVAM_API_KEY:
print("SARVAM_API_KEY is not set.")
exit(1)

# This dictionary mimics what get_provider_credential would return from the DB
mock_credentials = {"api_key": SARVAM_API_KEY}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove the hardcoded Sarvam API key from the test script.

Hardcoded secrets should never be committed, even in ad-hoc test blocks.

🔐 Suggested fix
-    SARVAM_API_KEY = "sk_lmsvfc31_On1bxqwDAqYZoijqBfblr3yf"  # for testing only
+    SARVAM_API_KEY = os.getenv("SARVAM_API_KEY")
🤖 Prompt for AI Agents
In
`@backend/app/tests/services/llm/providers/STTproviders/test_STT_SarvamProvider.py`
around lines 108 - 119, Remove the hardcoded Sarvam API key in the __main__ test
block: replace the literal assignment to SARVAM_API_KEY and construction of
mock_credentials with loading from a safe source (e.g.,
os.environ.get("SARVAM_API_KEY")) or a test fixture/mocked secret provider;
ensure the code validates presence and exits if missing (preserve the existing
if not SARVAM_API_KEY check) and update mock_credentials to use the sourced
value (mock_credentials = {"api_key": SARVAM_API_KEY}) so no secrets are
committed in test_STT_SarvamProvider.py.

@nishika26 nishika26 changed the title Feature/unifed api stt sarvam unifed api: stt sarvam Feb 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

TTS/STT: Integrate Sarvam as a Provider to the Unified API for STT

3 participants